package xperiment.metaphor.model.entity;

import java.util.*;
import javax.persistence.Transient;
import metaphor.service.ServiceException;
import metaphor.service.ApplicationManager;
import metaphor.persistence.filter.Query;
import metaphor.persistence.Dao;
import org.apache.commons.lang.builder.CompareToBuilder;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.time.DateUtils;
import org.springframework.context.ApplicationContext;
import xperiment.metaphor.model.*;

/**
 * Employee
 *
 * @author Mark Ashworth
 * @version 1.0.0
 * @since 1.0.0
 */
public class Employee extends DomainObject implements EmployeeRole, Persistent, Comparable, XML {

    /** The role name of the actor - role collaboration */
    public static final String ROLE_NAME = "ROLE_EMPLOYEE";

    /** An empty employee object */
    public static final Employee EMPTY = new Employee();

    /* The unique database id for the object */
    private Long id;

    /* The employee that this employee reports to */
    private Long reportsToId;

    /** The employee number */
	private String employeeNo;
    
	/** The position that the employee is in */
	private Position position;

    /** The date the employee joined the organization */
    private Date joined;

    /** The date the employment was terminated with the organization */
    private Date terminated;

    /** Enabled */
    private boolean enabled = true;

	/**
	 * Default constructor
	 */
	public Employee() {
		this(null, null);
	}
		
    /**
	 * Constructs the employee
     * @param employeeNo The employee's employment number
	 */
	public Employee(String employeeNo) {
		this(employeeNo, null);
	}
    
	/**
	 * Constructs the employee
     * @param employeeNo The employee's employment number
	 * @param position The employment position
	 */
	public Employee(String employeeNo, Position position) {
		super();
        this.employeeNo = employeeNo;
        this.position = position;
	}

    /**
     * The collaboration role of the Employee to the Person
     * @return String
     */
    public String getRole() {
        return ROLE_NAME;
    }

    /**
     * The unique database id for the object
     * @return Long
     */
	public Long getId() {
		return id;
	}

    /**
     * The employee that this employee reports to
     * @return Long
     */
    public Long getReportsToId() {
        return reportsToId;
    }
    
    /**
     * Employee number
     * @return String
     */
	public String getEmployeeNo() {
		return employeeNo;
	}

    /**
     * Employee number
     * @return String
     */
	public void setEmployeeNo(String employeeNo) {
        String old = getEmployeeNo();
		this.employeeNo = employeeNo;
        firePropertyChange("employeeNo", old, getEmployeeNo());
	}

    /**
     * Employment position of the employee
     * @return Position
     */
	public Position getPosition() {
		return position;
	}

    /**
     * The date the employee joined the organization
     * @return Date
     */
    public Date getJoined() {
        return joined;
    }

    /**
     * The date the employment was terminated with the organization
     * @return Date
     */
    public Date getTerminated() {
        return terminated;
    }

    /**
     * Whether the employee role is enabled or active in the system
     * @return boolean
     */
    public boolean isEnabled() {
		return enabled;
	}

    /**
     * Whether the employee role is enabled or active in the system
     * @param enabled The new value
     */
	protected void setEnabled(boolean enabled) {
		boolean old = this.enabled;
		this.enabled = enabled;
		firePropertyChange("enabled", old, this.enabled);
	}

    /**
     * Sets the employee number
     * @param employeeNo The new value
     * @return Employee
     */
    public Employee employeeNo(String employeeNo) {
        this.setEmployeeNo(employeeNo);
        return this;
    }

    /**
     * Sets the date that the employee joined the organization
     * @param date The new value
     * @return Employee
     */
    public Employee joined(Date date) {
        if (date != null) {
            this.joined = (Date) date.clone();
        } else {
            this.joined = null;
        }
        return this;
    }

    /**
     * Sets the date that the employee joined the organization
     * @param year The year (eg. 2008)
     * @param month The month (eg. 05 == May)
     * @param date The day
     * @return Employee
     */
    public Employee joined(int year, int month, int date) {
        if (month < 1 || month > 12) throw new IllegalArgumentException("The month of " + month + " is invalid.");

        Calendar cal = Calendar.getInstance();
        cal.clear();
        cal.set(year, month + 1, date);

        return joined(cal.getTime());
    }

    /**
     * Terminates the employee with the terminate date set to today.
     * The employee is also discharged from the current position.
     * @return Employee
     * @throws TerminateEmployeeException If there was an error terminating the employment
     * @throws ModelException If there was an error terminating the employment
     */
    public Employee terminate() throws ModelException {
        Calendar cal = Calendar.getInstance();
        DateUtils.truncate(cal, Calendar.DATE);
        return terminate(cal.getTime());
    }

    /**
     * Sets the date that the employment with the organization is terminated
     * @param year The year (eg. 2008)
     * @param month The month (eg. 05 == May)
     * @param date The day
     * @return Employee
     * @throws TerminateEmployeeException If there was an error terminating the employment
     */
    public Employee terminate(int year, int month, int date) throws ModelException {
        if (month < 1 || month > 12) throw new IllegalArgumentException("The month of " + month + " is invalid.");

        Calendar cal = Calendar.getInstance();
        cal.clear();
        cal.set(year, month + 1, date);

        return terminate(cal.getTime());
    }

    /**
     * Terminates the employee. The employee is also discharged from the current position.<br />
     *
     * @param date The new value
     * @return Employee
     * @throws TerminateEmployeeException If there was an error terminating the employment
     * @throws ModelException If there was an error terminating the employment
     */
    public Employee terminate(Date date) throws ModelException {
        if (date == null) {
            throw new TerminateEmployeeException("The terminate date may not be null");
        }

        /*
         * Terminate the employee record
         */
        this.terminated = (Date) date.clone();
        deactivate();

        /*
         * A terminated employee no longer holds a position so discharge from the position
         */
        if (getPosition() != null) {
            Position p = getPosition();
            p.discharge(this.terminated);
            p.attach(getApplicationManager());
            p.save();
        }

        return save();
    }

    /**
     * Sets the employee that this employee reports to.<br />
     * <strong>Note:The ApplicationManager must be set before calling this method</strong>
     * @param employee The employee that this employee reports to
     * @return Employee
     * @throws ReportsToEmployeeException If the employee is null or transient
     * @throws ModelException If there were errors saving the employee
     */
    public Employee reportsTo(Employee employee) throws ModelException {

        if (employee == null) {
            throw new ReportsToEmployeeException("The employee to report to may not be null");
        } else if (employee.getId() <= 0) {
            throw new ReportsToEmployeeException("The employ to report to is transient");
        }

        this.reportsToId = employee.getId();

        return save();
    }

    /**
     * Appoints the person to the position and sets the appointment date.<br />
     *
     * <strong>Note: This method requires that the applicationManager be set.</strong>
     *
     * @param position The position to appoint to
     * @param appointmentDate The date of the appointment
     * @return Employee
     * @throws ModelException If there was an error with appointing the person to a position
     */
    public Employee appoint(Position position, Date appointmentDate) throws ModelException {

        if (appointmentDate == null) throw new AppointmentException("The appointment date is null");

        if (position.getApproved() == null) {
            throw new AppointmentException("The position has not been approved");
        }
        if (position.getAppointed() != null) {
            throw new AppointmentException("The position is already appointed");
        }

        Position existing = this.getPosition();
        if (existing != null) {
            /* Calculate the discharge based upon the new appointment date - 1 */
            Calendar cal = Calendar.getInstance();
            cal.setTime(appointmentDate);
            DateUtils.truncate(cal, Calendar.DATE);
            cal.add(Calendar.DATE, -1);

            existing.discharge(cal.getTime());

            /* Save the existing position */
            existing.attach(getApplicationManager());
            existing.save();
        }

        /* Save the employee position details */
        position.attach(getApplicationManager());
        position.appoint(this, appointmentDate);

        /* Save the employee link to the position */
        this.position = position;
        return save();
    }

    /**
     * Sets the employee's enabled status to true
     * @return Employee
     */
    public Employee activate() {
        this.setEnabled(true);
        return this;
    }

    /**
     * Sets the employee's enabled status to false
     * @return Employee
     */
    public Employee deactivate() {
        this.setEnabled(false);
        return this;
    }

    public Employee save() throws SaveException {

        /*
         * Persist the position
         */
        if (getPosition() != null && getPosition().getId() == null) {
            throw new SaveTransientException("The position of the employee is transient. Please use Company, Department and Position to initialize this value");
        }


        /*
         * Persist the employee
         */
        try {
            Employee e = (Employee) getDao().persist(this);
            this.id = e.getId();
            return this;
        } catch (ServiceException e) {
            throw new SaveException("Unable to save employee", e);
        }
    }

    protected Query getEmploymentHistoryQuery() {
        return Query.forClass(Position.class).eq("employeeId", this.getId());
    }

    /**
     * Retrieves the employment history of the employee
     * @return List<Position>
     * @throws xperiment.metaphor.model.RetrieveException If there was an error
     */
    @SuppressWarnings("unchecked")
    public List<Position> employmentHistory() throws RetrieveException {

        Dao dao;

        try {
            dao = (Dao) getApplicationManager().getDao();
        } catch (ServiceException e) {
            throw new RetrieveException("Unable to retrieve employee history because of an application manager exception", e);
        }

        List values = dao.retrieve(getEmploymentHistoryQuery());
        if (values == null) {
            return new ArrayList<Position>();
        }
        return (List<Position>) values;
    }

    /**
     * Retrieves the employment history of the employee
     * @param applicationManager The application manager
     * @param employee The position history of the employee
     * @return List<Position>
     * @throws xperiment.metaphor.model.RetrieveException If there was an error
     */
    public static List<Position> employmentHistory(ApplicationManager applicationManager, Employee employee) throws RetrieveException {

        employee.attach(applicationManager);
        return employee.employmentHistory();
    }
    
    /**
     * Returns the XML representation of the person.<br />
     * @return String
     */
    public String toXML() {

        return "<employee='" + getClass().getName()
                + "' id='" + getId()
                + "' employee_no='" + getEmployeeNo()
                + "' enabled='" + isEnabled()
                + "' >"
                + (getPosition() != null ? getPosition().toXML() : "")
                + "</employee>";
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) { return false; }
        if (obj == this) { return true; }
        if (obj.getClass() != getClass()) {
            return false;
        }
        Employee rhs = (Employee) obj;
        return new EqualsBuilder()
            .append(this.employeeNo, rhs.getEmployeeNo())
            .isEquals();
    }

    @Override
    public int hashCode() {
        return new HashCodeBuilder(17, 37).append(this.employeeNo).toHashCode();
    }

    public int compareTo(Object o) {

        Employee other = (Employee) o;
        return new CompareToBuilder()
                .append(this.employeeNo, other.getEmployeeNo())
                .toComparison();
    }
}
